Skip to content

Backend ai#220

Open
Jackson57279 wants to merge 3 commits intomasterfrom
backend-ai
Open

Backend ai#220
Jackson57279 wants to merge 3 commits intomasterfrom
backend-ai

Conversation

@Jackson57279
Copy link
Copy Markdown
Collaborator

@Jackson57279 Jackson57279 commented Apr 4, 2026

Summary by CodeRabbit

  • New Features

    • Automated backend implementation flow: schema proposals, AI-generated backend files, and optional preloading into projects to enable full-stack scaffolding.
    • Full-stack prompt support for Next.js + Convex and smarter prompt/context handling for coding agents.
    • Prompt-based detection to suggest/trigger backend setup and attach backend metadata to results.
    • Request flow now forwards message identifiers for end-to-end tracing.
  • Chores

    • Updated AI dependency version.

- Update 'ai' package version to 6.0.146 for improved functionality
- Introduce optional backend support in project creation and message handling
- Add 'setHasBackendForUser' mutation to manage backend status for projects
- Extend message creation to include optional messageId for better tracking
- Update API definitions to include new schema proposals and backend features
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

Adds an automated Convex backend bootstrapping flow: schema-proposal and backend-generation agents, new prompts, schemaProposals table and related mutations, Convex project/fragment backend metadata, and integration into the Inngest orchestration and client request paths.

Changes

Cohort / File(s) Summary
Backend Agents
src/agents/schema-proposal-agent.ts, src/agents/backend-agent.ts, src/agents/wants-backend.ts, src/agents/index.ts
New agents for schema proposals and backend file generation, backend-intent detection, and re-exports. Review parsing logic, model call configs, and exported typings.
Backend Prompts
src/prompts/backend/schema-proposal.ts, src/prompts/backend/convex-backend.ts, src/prompts/backend/fullstack.ts, src/prompts/backend/index.ts, src/prompt.ts
Added three large prompt templates and centralized re-exports; inspect prompt content for required output tagging and any assumptions about environment or file templates.
Convex Schema & Mutations
convex/schema.ts, convex/schemaProposals.ts, convex/projects.ts, convex/messages.ts
New schemaProposals table and enum, added hasBackend/backendFiles fields, new setHasBackendForUser mutation, and extended createFragmentForUser args/patching. Check auth/ownership checks and index additions.
Orchestration & Agent Wiring
src/inngest/functions.ts, src/app/api/agent/run/route.ts
Integrated backend bootstrapping into agent flow: detect intent, run schema proposal → backend implementer, persist proposals, preload files, and flag project as having a backend. messageId is threaded through requests. Review control flow, error handling, and persistence ordering.
Client UI
src/modules/home/ui/components/project-form.tsx, src/modules/projects/ui/components/message-form.tsx
Frontend now includes messageId when calling the agent run API. Confirm message creation timing and returned id usage.
Agent Prompt Surface
src/prompt.ts
Re-exported new backend-related prompt constants.
State & Dependencies
.cursor/hooks/state/continual-learning.json, package.json
Added initial continual-learning state file; bumped ai dependency from ^6.0.5 to ^6.0.146. Verify dependency compatibility.
Exports Indexing
src/agents/index.ts, src/prompts/backend/index.ts
Re-exports added to surface new agent functions and backend prompts. Confirm no naming collisions.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Client
    participant Inngest
    participant OpenRouter
    participant Convex

    User->>Client: Submit request (includes messageId)
    Client->>Inngest: POST /api/agent/run (projectId, value, messageId)
    Inngest->>Inngest: wantsConvexBackend(prompt) check
    alt needs backend
        Inngest->>OpenRouter: runSchemaProposalAgent(prompt, plan)
        OpenRouter-->>Inngest: <schema_proposal>...</schema_proposal>
        Inngest->>Inngest: parseSchemaProposal -> parsedTables
        Inngest->>Convex: create schemaProposals record
        Inngest->>OpenRouter: runBackendImplementerAgent(prompt, schemaProposal)
        OpenRouter-->>Inngest: generated files (<zapdev_file ...>)
        Inngest->>Inngest: parseGeneratedFiles -> backendPreload
        Inngest->>Convex: mark proposal implemented
        Inngest->>Convex: setHasBackendForUser(hasBackend=true)
    end
    Inngest->>Convex: createFragmentForUser(..., hasBackend?, backendFiles?)
    Inngest-->>Client: respond with result and backend metadata
    Client-->>User: display results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • feat/figma github imports #130: Overlaps Convex schema/mutation changes (convex/schema.ts, convex/messages.ts, convex/projects.ts) — closely related to backend metadata and schema updates.
  • adding new frameworks #109: Related changes to prompt exports and agent-driven code-generation flow; intersects prompt/agent surfaces.

Suggested labels

capy

Poem

🐰 I found a prompt and gave it a hop,

schemas bloomed and backend files did drop.
From message id to Convex land,
I stitched the code with careful hand.
Hooray — full-stack carrots for the devs to crop! 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Backend ai' is vague and generic, failing to clearly summarize the main changes in the changeset which include schema proposals, backend code generation, and fullstack integration. Use a more descriptive title that captures the primary feature, such as 'Add schema proposal and backend code generation for Convex' or 'Implement AI-driven backend bootstrapping workflow'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch backend-ai

Comment @coderabbitai help to get the list of available commands and usage tips.

@Jackson57279
Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai Bot commented Apr 4, 2026

@cubic-dev-ai review

@Jackson57279 I have started the AI code review. It will take a few minutes to complete.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (3)
src/agents/wants-backend.ts (1)

1-3: Backend intent regex is likely too broad and may over-trigger.

Line 2 includes generic terms (queries, mutations, schema) that can appear in non-backend requests, which may cause unnecessary backend bootstrapping. Consider splitting into “strong backend signals” vs “weak signals” and requiring at least one strong match.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agents/wants-backend.ts` around lines 1 - 3, The BACKEND_INTENT regex is
too broad; replace it with two regexes (e.g., STRONG_BACKEND_INTENT and
WEAK_BACKEND_INTENT) and change the matching logic to require at least one
strong signal to trigger backend bootstrapping (do not trigger on weak-only
matches). Update the declaration that currently defines BACKEND_INTENT to
instead define the two regex constants (moving strong anchors like "database",
"backend", "authentication", "postgres", "supabase", "prisma" into
STRONG_BACKEND_INTENT and generic terms like "queries", "mutations", "schema"
into WEAK_BACKEND_INTENT) and modify every usage site that references
BACKEND_INTENT (search for BACKEND_INTENT in this file/agent logic) to perform a
check like: if (STRONG_BACKEND_INTENT.test(text)) { bootstrap } else { do not
bootstrap } (or include additional conditional logic only if you want to allow
weak signals combined with other heuristics).
convex/schema.ts (1)

132-133: backendFiles should be schema-validated more strictly than v.any().

Using v.any() here weakens data guarantees and makes malformed payloads easy to persist. Prefer a constrained shape (for example, a filename→content record) to protect reads and future migrations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/schema.ts` around lines 132 - 133, The backendFiles field uses v.any()
which is too permissive; replace v.optional(v.any()) with a constrained schema
(e.g., v.optional(v.record(v.string(), v.string())) or
v.optional(v.dict(v.string(), v.string())) depending on your validator API) to
enforce a filename→content shape, and update any code that reads backendFiles
accordingly; keep hasBackend as v.optional(v.boolean()) but ensure backendFiles
validation matches the expected structure used by functions that access
backendFiles.
convex/schemaProposals.ts (1)

59-64: Consider: approvedAt is set on implementation even if never explicitly approved.

Line 61 sets approvedAt: row.approvedAt ?? now, meaning if a schema was never approved but is directly marked as implemented, it gets an approvedAt timestamp. This may be intentional (auto-approve on implement) but could mask the approval workflow if auditing requires explicit approval before implementation.

If explicit approval is required:

+    if (!row.approvedAt) {
+      throw new Error("Schema must be approved before implementation");
+    }
     await ctx.db.patch(args.schemaProposalId, {
       status: "IMPLEMENTED",
-      approvedAt: row.approvedAt ?? now,
+      approvedAt: row.approvedAt,
       implementedAt: now,
       updatedAt: now,
     });

Otherwise, if auto-approve is desired, consider adding a comment clarifying the intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/schemaProposals.ts` around lines 59 - 64, The patch sets approvedAt to
row.approvedAt ?? now when marking a proposal IMPLEMENTED, which implicitly
auto-approves proposals that were never explicitly approved; decide the intended
behavior and implement it: if explicit approval is required, change the update
logic (for the ctx.db.patch call using args.schemaProposalId and fields
approvedAt/status/implementedAt/updatedAt) to NOT set approvedAt when
row.approvedAt is absent and instead reject or surface an error, or else leave
approvedAt null; if auto-approve is intended, add a clarifying comment next to
the approvedAt assignment explaining that implementing also sets approvedAt to
now when missing to indicate auto-approval.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.cursor/hooks/state/continual-learning.json:
- Around line 1-8: Add a gitignore rule to exclude the IDE/state files under
.cursor/hooks/state/ by appending an entry like `.cursor/hooks/state/` to
.gitignore; ensure you only ignore the state directory (not other .cursor files
like rules or configuration) and include a short comment explaining it's to
prevent committing auto-generated IDE state files so reviewers understand the
intent.

In `@convex/projects.ts`:
- Around line 475-490: The mutation setHasBackendForUser currently trusts the
caller-supplied userId arg for authorization which is spoofable; update the
handler to call requireAuth(ctx) (or otherwise read ctx.auth.userId) and use the
authenticated user id to authorize (compare ctx.auth.userId to project.userId)
instead of args.userId, and remove or ignore the userId arg in args; also ensure
you still fetch the project via ctx.db.get(args.projectId) and throw
Unauthorized if the authenticated user isn't the owner before patching
hasBackend/updatedAt.

In `@src/agents/backend-agent.ts`:
- Around line 100-104: The loop that consumes model output (while ((match =
fileRegex.exec(text)) ...) trusts match[1] as a file path and writes it into
files[path]; instead validate and normalize each path before accepting: reject
absolute paths or any path containing "..", null bytes, or path separators that
escape the intended workspace, enforce an allowlist/prefix (e.g., must start
with "convex/"), and normalize (using a path normalization helper like
validateAndNormalizePath or path.posix.normalize) to collapse any "../" before
adding to files; if validation fails, skip the entry and log or surface an error
instead of writing to files.

In `@src/agents/schema-proposal-agent.ts`:
- Around line 50-57: The current validation in schema-proposal-agent.ts only
checks for the opening "<schema_proposal>" in the variable text which allows
malformed or truncated model output to be treated as successful; update the
validation in the handler that returns { schemaProposal, success, error } to
ensure both the opening "<schema_proposal>" and the corresponding closing
"</schema_proposal>" exist and that the content between them can be extracted
(e.g., locate start/end indices or use a simple regex to parse the block) before
setting success=true and returning schemaProposal; if parsing fails or the
closing tag is missing, return success=false and an explanatory error.
- Around line 121-123: The current regex in fieldsSection.matchAll uses (\w+)
which only captures word tokens and drops complex Convex validator expressions;
update the pattern to capture the entire validator expression (e.g. /-
`([^`]+)`: ([^\n]+)/g or /- `([^`]+)`: (.+)/g) and then trim the captured group
before pushing into fields so validators like v.id("users") or
v.optional(v.string()) are preserved (change the match in the
fieldsSection.matchAll call and use fields.push(`${fieldMatch[1]}:
${fieldMatch[2].trim()}`) in the same block that handles fieldMatches).

In `@src/app/api/agent/run/route.ts`:
- Around line 40-43: The messageId validation checks messageId.trim() but
forwards the original untrimmed messageId; update the assignment for the
messageId property so it sends the trimmed value (e.g., use messageId.trim())
when typeof messageId === "string" and trimmed length > 0. Modify the object
construction in the route handler where messageId is set to ensure the trimmed
string is forwarded (reference the messageId variable and the messageId:
property in this file). Ensure you still set undefined when the trimmed value is
empty.

In `@src/inngest/functions.ts`:
- Around line 284-296: The code unsafely casts event.data.messageId to
Id<"messages"> when resolving triggerMessageId; instead validate the incoming ID
before casting (or avoid casting) to prevent opaque failures: update the
resolve-trigger-message block (referencing triggerMessageId and
event.data.messageId) to check the ID format/shape used by Convex (or run a
lightweight lookup via getConvexClient + api.messages.listForUser) and only
return a typed Id<"messages"> when the value is valid, otherwise return null;
alternatively, keep the value as string | null and let the downstream mutation
(createForUser / ownership checks) surface a clear "Message not found" error.
- Around line 338-350: The current finalize-backend-bootstrap step runs two
sequential mutations
(convex.mutation(api.schemaProposals.markImplementedForUser, ...) and
convex.mutation(api.projects.setHasBackendForUser, ...)) which can leave the
system inconsistent if one succeeds and the other fails; change this by
splitting the work into two independent Inngest steps (e.g.,
finalize-mark-implemented and finalize-set-has-backend) so each mutation has its
own retry semantics, or alternatively make the operations atomic/idempotent in
the backend (use a transaction or an idempotent setter on
api.projects.setHasBackendForUser and check/update schemaProposal state to be
safe on retries). Ensure the new steps reference the same identifiers (userId,
schemaProposalId/schemaProposalRowId, projectId) and that needsBackendBootstrap
continues to consult project.hasBackend so workflow resumes correctly after
partial failures.

In `@src/prompts/backend/convex-backend.ts`:
- Around line 194-199: The prompt in src/prompts/backend/convex-backend.ts uses
createOrUpdateFiles + a <task_summary> block which conflicts with the parser in
src/agents/backend-agent.ts that expects <zapdev_file path="..."> blocks; update
the prompt to emit each file wrapped in <zapdev_file path="..."> with file
contents (one block per file) and move the summary outside or into a separate,
parser-recognized tag so the backend-agent.ts parser can extract files correctly
instead of producing empty parsed files.
- Around line 42-45: The prompt examples incorrectly show ctx.db.delete(id)
without the required table name; update the guidance so all CRUD examples
consistently use the correct signatures: ctx.db.insert("table", data),
ctx.db.patch("table", id, updates), ctx.db.replace("table", id, data), and
ctx.db.delete("table", id). Specifically change the ctx.db.delete example to
include the "table" parameter to match the other examples and avoid generating
invalid code.

---

Nitpick comments:
In `@convex/schema.ts`:
- Around line 132-133: The backendFiles field uses v.any() which is too
permissive; replace v.optional(v.any()) with a constrained schema (e.g.,
v.optional(v.record(v.string(), v.string())) or v.optional(v.dict(v.string(),
v.string())) depending on your validator API) to enforce a filename→content
shape, and update any code that reads backendFiles accordingly; keep hasBackend
as v.optional(v.boolean()) but ensure backendFiles validation matches the
expected structure used by functions that access backendFiles.

In `@convex/schemaProposals.ts`:
- Around line 59-64: The patch sets approvedAt to row.approvedAt ?? now when
marking a proposal IMPLEMENTED, which implicitly auto-approves proposals that
were never explicitly approved; decide the intended behavior and implement it:
if explicit approval is required, change the update logic (for the ctx.db.patch
call using args.schemaProposalId and fields
approvedAt/status/implementedAt/updatedAt) to NOT set approvedAt when
row.approvedAt is absent and instead reject or surface an error, or else leave
approvedAt null; if auto-approve is intended, add a clarifying comment next to
the approvedAt assignment explaining that implementing also sets approvedAt to
now when missing to indicate auto-approval.

In `@src/agents/wants-backend.ts`:
- Around line 1-3: The BACKEND_INTENT regex is too broad; replace it with two
regexes (e.g., STRONG_BACKEND_INTENT and WEAK_BACKEND_INTENT) and change the
matching logic to require at least one strong signal to trigger backend
bootstrapping (do not trigger on weak-only matches). Update the declaration that
currently defines BACKEND_INTENT to instead define the two regex constants
(moving strong anchors like "database", "backend", "authentication", "postgres",
"supabase", "prisma" into STRONG_BACKEND_INTENT and generic terms like
"queries", "mutations", "schema" into WEAK_BACKEND_INTENT) and modify every
usage site that references BACKEND_INTENT (search for BACKEND_INTENT in this
file/agent logic) to perform a check like: if (STRONG_BACKEND_INTENT.test(text))
{ bootstrap } else { do not bootstrap } (or include additional conditional logic
only if you want to allow weak signals combined with other heuristics).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4bfbc700-dbbb-4723-bed4-4a579d01cbea

📥 Commits

Reviewing files that changed from the base of the PR and between 840f4c4 and 739784e.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (19)
  • .cursor/hooks/state/continual-learning.json
  • convex/messages.ts
  • convex/projects.ts
  • convex/schema.ts
  • convex/schemaProposals.ts
  • package.json
  • src/agents/backend-agent.ts
  • src/agents/index.ts
  • src/agents/schema-proposal-agent.ts
  • src/agents/wants-backend.ts
  • src/app/api/agent/run/route.ts
  • src/inngest/functions.ts
  • src/modules/home/ui/components/project-form.tsx
  • src/modules/projects/ui/components/message-form.tsx
  • src/prompt.ts
  • src/prompts/backend/convex-backend.ts
  • src/prompts/backend/fullstack.ts
  • src/prompts/backend/index.ts
  • src/prompts/backend/schema-proposal.ts

Comment on lines +1 to +8
{
"version": 1,
"lastRunAtMs": 0,
"turnsSinceLastRun": 1,
"lastTranscriptMtimeMs": null,
"lastProcessedGenerationId": "02f2727d-db77-45c6-bb2d-4163883b764f",
"trialStartedAtMs": null
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

git rm .cursor/hooks/state/continual-learning.json

Repository: Zapdev-labs/zapdev

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Description: Check .gitignore for .cursor/ entry and find other .cursor files

echo "=== Checking .gitignore for .cursor/ entry ==="
if [ -f .gitignore ]; then
  rg -n '\.cursor' .gitignore || echo "No .cursor entry found in .gitignore"
else
  echo ".gitignore file not found"
fi

echo -e "\n=== Finding all .cursor files in the repository ==="
fd -H -t f . .cursor 2>/dev/null || echo "No .cursor files found"

Repository: Zapdev-labs/zapdev

Length of output: 321


Add .cursor/hooks/state/ to .gitignore to prevent committing IDE state files.

The continual-learning.json file has been removed from the PR. However, to prevent similar auto-generated IDE state files from being accidentally committed in the future, add the following to .gitignore:

+# Cursor IDE state files
+.cursor/hooks/state/

Note: The other .cursor/ files (rules and configuration) appear to be intentional and should remain in the repository. Only the state files in .cursor/hooks/state/ should be excluded.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.cursor/hooks/state/continual-learning.json around lines 1 - 8, Add a
gitignore rule to exclude the IDE/state files under .cursor/hooks/state/ by
appending an entry like `.cursor/hooks/state/` to .gitignore; ensure you only
ignore the state directory (not other .cursor files like rules or configuration)
and include a short comment explaining it's to prevent committing auto-generated
IDE state files so reviewers understand the intent.

Comment thread convex/projects.ts
Comment on lines +475 to +490
export const setHasBackendForUser = mutation({
args: {
userId: v.string(),
projectId: v.id("projects"),
hasBackend: v.boolean(),
},
handler: async (ctx, args) => {
const project = await ctx.db.get(args.projectId);
if (!project || project.userId !== args.userId) {
throw new Error("Unauthorized");
}
await ctx.db.patch(args.projectId, {
hasBackend: args.hasBackend,
updatedAt: Date.now(),
});
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Authorization is bypassable because identity is caller-controlled.

Line 477 takes userId from input, and Lines 483-484 trust it for auth without requireAuth(ctx). This permits spoofing ownership checks.

Suggested fix (bind auth to session, not args)
 export const setHasBackendForUser = mutation({
   args: {
-    userId: v.string(),
     projectId: v.id("projects"),
     hasBackend: v.boolean(),
   },
   handler: async (ctx, args) => {
+    const userId = await requireAuth(ctx);
     const project = await ctx.db.get(args.projectId);
-    if (!project || project.userId !== args.userId) {
+    if (!project || project.userId !== userId) {
       throw new Error("Unauthorized");
     }
     await ctx.db.patch(args.projectId, {
       hasBackend: args.hasBackend,
       updatedAt: Date.now(),
     });
   },
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const setHasBackendForUser = mutation({
args: {
userId: v.string(),
projectId: v.id("projects"),
hasBackend: v.boolean(),
},
handler: async (ctx, args) => {
const project = await ctx.db.get(args.projectId);
if (!project || project.userId !== args.userId) {
throw new Error("Unauthorized");
}
await ctx.db.patch(args.projectId, {
hasBackend: args.hasBackend,
updatedAt: Date.now(),
});
},
export const setHasBackendForUser = mutation({
args: {
projectId: v.id("projects"),
hasBackend: v.boolean(),
},
handler: async (ctx, args) => {
const userId = await requireAuth(ctx);
const project = await ctx.db.get(args.projectId);
if (!project || project.userId !== userId) {
throw new Error("Unauthorized");
}
await ctx.db.patch(args.projectId, {
hasBackend: args.hasBackend,
updatedAt: Date.now(),
});
},
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/projects.ts` around lines 475 - 490, The mutation setHasBackendForUser
currently trusts the caller-supplied userId arg for authorization which is
spoofable; update the handler to call requireAuth(ctx) (or otherwise read
ctx.auth.userId) and use the authenticated user id to authorize (compare
ctx.auth.userId to project.userId) instead of args.userId, and remove or ignore
the userId arg in args; also ensure you still fetch the project via
ctx.db.get(args.projectId) and throw Unauthorized if the authenticated user
isn't the owner before patching hasBackend/updatedAt.

Comment on lines +100 to +104
while ((match = fileRegex.exec(text)) !== null) {
const path = match[1];
const content = match[2].trim();
files[path] = content;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate generated file paths before accepting them.

path from model output is trusted as-is. A prompt-injected response can emit unexpected targets (../, hidden config paths, etc.). Restrict to an allowlist/prefix (for example convex/) and normalize/reject unsafe paths before adding them to files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agents/backend-agent.ts` around lines 100 - 104, The loop that consumes
model output (while ((match = fileRegex.exec(text)) ...) trusts match[1] as a
file path and writes it into files[path]; instead validate and normalize each
path before accepting: reject absolute paths or any path containing "..", null
bytes, or path separators that escape the intended workspace, enforce an
allowlist/prefix (e.g., must start with "convex/"), and normalize (using a path
normalization helper like validateAndNormalizePath or path.posix.normalize) to
collapse any "../" before adding to files; if validation fails, skip the entry
and log or surface an error instead of writing to files.

Comment on lines +50 to +57
if (!text.includes("<schema_proposal>")) {
console.error("[SCHEMA PROPOSAL] Invalid response - missing schema_proposal tag");
return {
schemaProposal: text,
success: false,
error: "Invalid schema proposal format",
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Success validation is too weak for malformed/truncated model output.

Checking only the opening tag can return success: true even when the closing tag/content is missing. Validate both opening and closing tags (or parse block first) before marking success.

Suggested guard
-    if (!text.includes("<schema_proposal>")) {
+    const hasProposalBlock = /<schema_proposal>[\s\S]*<\/schema_proposal>/.test(text);
+    if (!hasProposalBlock) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!text.includes("<schema_proposal>")) {
console.error("[SCHEMA PROPOSAL] Invalid response - missing schema_proposal tag");
return {
schemaProposal: text,
success: false,
error: "Invalid schema proposal format",
};
}
const hasProposalBlock = /<schema_proposal>[\s\S]*<\/schema_proposal>/.test(text);
if (!hasProposalBlock) {
console.error("[SCHEMA PROPOSAL] Invalid response - missing schema_proposal tag");
return {
schemaProposal: text,
success: false,
error: "Invalid schema proposal format",
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agents/schema-proposal-agent.ts` around lines 50 - 57, The current
validation in schema-proposal-agent.ts only checks for the opening
"<schema_proposal>" in the variable text which allows malformed or truncated
model output to be treated as successful; update the validation in the handler
that returns { schemaProposal, success, error } to ensure both the opening
"<schema_proposal>" and the corresponding closing "</schema_proposal>" exist and
that the content between them can be extracted (e.g., locate start/end indices
or use a simple regex to parse the block) before setting success=true and
returning schemaProposal; if parsing fails or the closing tag is missing, return
success=false and an explanatory error.

Comment on lines +121 to +123
const fieldMatches = fieldsSection.matchAll(/- `([^`]+)`: (\w+)/g);
for (const fieldMatch of fieldMatches) {
fields.push(`${fieldMatch[1]}: ${fieldMatch[2]}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Field parsing regex drops real Convex validator types.

(\w+) only captures word tokens, so validators like v.id("users") or v.optional(v.string()) are truncated. This degrades parsedTables.fields quality and downstream proposal metadata.

Suggested regex hardening
-    const fieldMatches = fieldsSection.matchAll(/- `([^`]+)`: (\w+)/g);
+    const fieldMatches = fieldsSection.matchAll(/- `([^`]+)`: ([^-\\n]+?)(?:\\s-\\s.*)?$/gm);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fieldMatches = fieldsSection.matchAll(/- `([^`]+)`: (\w+)/g);
for (const fieldMatch of fieldMatches) {
fields.push(`${fieldMatch[1]}: ${fieldMatch[2]}`);
const fieldMatches = fieldsSection.matchAll(/- `([^`]+)`: ([^-\n]+?)(?:\s-\s.*)?$/gm);
for (const fieldMatch of fieldMatches) {
fields.push(`${fieldMatch[1]}: ${fieldMatch[2]}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agents/schema-proposal-agent.ts` around lines 121 - 123, The current
regex in fieldsSection.matchAll uses (\w+) which only captures word tokens and
drops complex Convex validator expressions; update the pattern to capture the
entire validator expression (e.g. /- `([^`]+)`: ([^\n]+)/g or /- `([^`]+)`:
(.+)/g) and then trim the captured group before pushing into fields so
validators like v.id("users") or v.optional(v.string()) are preserved (change
the match in the fieldsSection.matchAll call and use
fields.push(`${fieldMatch[1]}: ${fieldMatch[2].trim()}`) in the same block that
handles fieldMatches).

Comment on lines +40 to +43
messageId:
typeof messageId === "string" && messageId.trim().length > 0
? messageId
: undefined,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Trimmed validation is not applied to the value being sent.

Line 41 validates messageId.trim() but Line 42 forwards untrimmed messageId. This can leak whitespace into downstream lookups.

Suggested fix
         messageId:
           typeof messageId === "string" && messageId.trim().length > 0
-            ? messageId
+            ? messageId.trim()
             : undefined,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
messageId:
typeof messageId === "string" && messageId.trim().length > 0
? messageId
: undefined,
messageId:
typeof messageId === "string" && messageId.trim().length > 0
? messageId.trim()
: undefined,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/agent/run/route.ts` around lines 40 - 43, The messageId
validation checks messageId.trim() but forwards the original untrimmed
messageId; update the assignment for the messageId property so it sends the
trimmed value (e.g., use messageId.trim()) when typeof messageId === "string"
and trimmed length > 0. Modify the object construction in the route handler
where messageId is set to ensure the trimmed string is forwarded (reference the
messageId variable and the messageId: property in this file). Ensure you still
set undefined when the trimmed value is empty.

Comment thread src/inngest/functions.ts
Comment on lines +284 to +296
const triggerMessageId = await step.run("resolve-trigger-message", async () => {
const explicit = event.data.messageId as string | undefined;
if (explicit && explicit.length > 0) {
return explicit as Id<"messages">;
}
const convex = getConvexClient();
const messages = await convex.query(api.messages.listForUser, {
userId,
projectId,
});
const lastUser = [...messages].reverse().find((m) => m.role === "USER");
return lastUser?._id ?? null;
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unsafe cast of messageId to Id<"messages">.

The event.data.messageId string is cast directly to Id<"messages"> after only checking that it has length > 0. If the string is not a valid Convex document ID, the subsequent mutation at line 311 will fail with an opaque error. Consider validating the ID format or letting the mutation's ownership check (line 29-31 in schemaProposals.ts) handle it gracefully.

🛡️ Proposed defensive check
       const explicit = event.data.messageId as string | undefined;
-      if (explicit && explicit.length > 0) {
+      if (explicit && explicit.length > 0 && explicit.startsWith("k")) {
+        // Basic Convex ID format check - IDs typically start with specific prefixes
         return explicit as Id<"messages">;
       }

Note: Alternatively, rely on the existing message ownership validation in createForUser to throw "Message not found" for invalid IDs.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const triggerMessageId = await step.run("resolve-trigger-message", async () => {
const explicit = event.data.messageId as string | undefined;
if (explicit && explicit.length > 0) {
return explicit as Id<"messages">;
}
const convex = getConvexClient();
const messages = await convex.query(api.messages.listForUser, {
userId,
projectId,
});
const lastUser = [...messages].reverse().find((m) => m.role === "USER");
return lastUser?._id ?? null;
});
const triggerMessageId = await step.run("resolve-trigger-message", async () => {
const explicit = event.data.messageId as string | undefined;
if (explicit && explicit.length > 0 && explicit.startsWith("k")) {
// Basic Convex ID format check - IDs typically start with specific prefixes
return explicit as Id<"messages">;
}
const convex = getConvexClient();
const messages = await convex.query(api.messages.listForUser, {
userId,
projectId,
});
const lastUser = [...messages].reverse().find((m) => m.role === "USER");
return lastUser?._id ?? null;
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/inngest/functions.ts` around lines 284 - 296, The code unsafely casts
event.data.messageId to Id<"messages"> when resolving triggerMessageId; instead
validate the incoming ID before casting (or avoid casting) to prevent opaque
failures: update the resolve-trigger-message block (referencing triggerMessageId
and event.data.messageId) to check the ID format/shape used by Convex (or run a
lightweight lookup via getConvexClient + api.messages.listForUser) and only
return a typed Id<"messages"> when the value is valid, otherwise return null;
alternatively, keep the value as string | null and let the downstream mutation
(createForUser / ownership checks) surface a clear "Message not found" error.

Comment thread src/inngest/functions.ts
Comment on lines +338 to +350
await step.run("finalize-backend-bootstrap", async () => {
const convex = getConvexClient();
await convex.mutation(api.schemaProposals.markImplementedForUser, {
userId,
schemaProposalId: schemaProposalRowId,
});
await convex.mutation(api.projects.setHasBackendForUser, {
userId,
projectId,
hasBackend: true,
});
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Non-atomic mutations may leave inconsistent state.

The finalize-backend-bootstrap step runs two mutations sequentially. If markImplementedForUser succeeds but setHasBackendForUser fails (e.g., network error, rate limit), the schema proposal is marked IMPLEMENTED while project.hasBackend remains false. On retry, the entire bootstrap flow would be skipped (since needsBackendBootstrap checks !project.hasBackend), leaving the schema proposal orphaned as "implemented" without actual project update.

💡 Suggested approach: split into separate steps or add idempotency
-          await step.run("finalize-backend-bootstrap", async () => {
-            const convex = getConvexClient();
-            await convex.mutation(api.schemaProposals.markImplementedForUser, {
-              userId,
-              schemaProposalId: schemaProposalRowId,
-            });
-            await convex.mutation(api.projects.setHasBackendForUser, {
-              userId,
-              projectId,
-              hasBackend: true,
-            });
-          });
+          await step.run("mark-schema-implemented", async () => {
+            const convex = getConvexClient();
+            await convex.mutation(api.schemaProposals.markImplementedForUser, {
+              userId,
+              schemaProposalId: schemaProposalRowId,
+            });
+          });
+          await step.run("set-project-has-backend", async () => {
+            const convex = getConvexClient();
+            await convex.mutation(api.projects.setHasBackendForUser, {
+              userId,
+              projectId,
+              hasBackend: true,
+            });
+          });

By splitting into separate Inngest steps, each mutation gets independent retry semantics and the workflow can resume from the correct point after partial failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/inngest/functions.ts` around lines 338 - 350, The current
finalize-backend-bootstrap step runs two sequential mutations
(convex.mutation(api.schemaProposals.markImplementedForUser, ...) and
convex.mutation(api.projects.setHasBackendForUser, ...)) which can leave the
system inconsistent if one succeeds and the other fails; change this by
splitting the work into two independent Inngest steps (e.g.,
finalize-mark-implemented and finalize-set-has-backend) so each mutation has its
own retry semantics, or alternatively make the operations atomic/idempotent in
the backend (use a transaction or an idempotent setter on
api.projects.setHasBackendForUser and check/update schemaProposal state to be
safe on retries). Ensure the new steps reference the same identifiers (userId,
schemaProposalId/schemaProposalRowId, projectId) and that needsBackendBootstrap
continues to consult project.hasBackend so workflow resumes correctly after
partial failures.

Comment on lines +42 to +45
- Use ctx.db.insert("table", data) to create
- Use ctx.db.patch("table", id, updates) for partial updates
- Use ctx.db.replace("table", id, data) for full replacement
- Use ctx.db.delete(id) for deletion
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What are the current Convex method signatures for ctx.db.patch, ctx.db.replace, and ctx.db.delete inside mutations?

💡 Result:

The current Convex method signatures for ctx.db inside mutations (GenericDatabaseWriter) are: ctx.db.patch(table: TableName, id: GenericId, value: PatchValue<DocumentByName<DataModel, TableName>>): Promise - Patches an existing document with a shallow merge. Adds new fields, overwrites existing, removes fields set to undefined. Throws if document does not exist. ctx.db.replace(table: TableName, id: GenericId, value: DocumentByName<DataModel, TableName>): Promise (inferred) - Replaces the entire document content, overwriting old value completely. Fields not included are removed. ctx.db.delete(table: TableName, id: GenericId): Promise - Deletes the document by ID. Throws if document does not exist. Note: Legacy overloads without table name (ctx.db.patch(id, value), etc.) exist but are deprecated since convex 1.31.0 (Dec 2025). Current best practice requires explicit table name first. All operations are async and transactional within mutations.

Citations:


ctx.db.delete(id) call is missing the required table parameter.

The delete method shown in the prompt omits the table name as the first argument. The correct signature is ctx.db.delete("table", id). The current guidance will generate invalid code that either fails or relies on deprecated behavior.

The patch and replace method signatures shown are correct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/prompts/backend/convex-backend.ts` around lines 42 - 45, The prompt
examples incorrectly show ctx.db.delete(id) without the required table name;
update the guidance so all CRUD examples consistently use the correct
signatures: ctx.db.insert("table", data), ctx.db.patch("table", id, updates),
ctx.db.replace("table", id, data), and ctx.db.delete("table", id). Specifically
change the ctx.db.delete example to include the "table" parameter to match the
other examples and avoid generating invalid code.

Comment on lines +194 to +199
Use createOrUpdateFiles to write all files. After all files are created, output:

<task_summary>
Generated Convex backend with [N] tables: [table names]. Created [N] query functions, [N] mutation functions, and [N] actions. All functions include proper authentication, authorization, and error handling.
</task_summary>
`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Output contract here conflicts with the backend parser contract.

This prompt asks for createOrUpdateFiles + <task_summary>, but src/agents/backend-agent.ts parses <zapdev_file path="..."> blocks. This mismatch can yield empty parsed files and false failure paths.

Suggested prompt contract alignment
-Use createOrUpdateFiles to write all files. After all files are created, output:
+Output all generated files using explicit XML-style file blocks:
+
+<zapdev_file path="convex/schema.ts">
+...full file contents...
+</zapdev_file>
+
+Repeat for every generated file, then output:
 
 <task_summary>
 Generated Convex backend with [N] tables: [table names]. Created [N] query functions, [N] mutation functions, and [N] actions. All functions include proper authentication, authorization, and error handling.
 </task_summary>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Use createOrUpdateFiles to write all files. After all files are created, output:
<task_summary>
Generated Convex backend with [N] tables: [table names]. Created [N] query functions, [N] mutation functions, and [N] actions. All functions include proper authentication, authorization, and error handling.
</task_summary>
`;
Output all generated files using explicit XML-style file blocks:
<zapdev_file path="convex/schema.ts">
...full file contents...
</zapdev_file>
Repeat for every generated file, then output:
<task_summary>
Generated Convex backend with [N] tables: [table names]. Created [N] query functions, [N] mutation functions, and [N] actions. All functions include proper authentication, authorization, and error handling.
</task_summary>
`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/prompts/backend/convex-backend.ts` around lines 194 - 199, The prompt in
src/prompts/backend/convex-backend.ts uses createOrUpdateFiles + a
<task_summary> block which conflicts with the parser in
src/agents/backend-agent.ts that expects <zapdev_file path="..."> blocks; update
the prompt to emit each file wrapped in <zapdev_file path="..."> with file
contents (one block per file) and move the summary outside or into a separate,
parser-recognized tag so the backend-agent.ts parser can extract files correctly
instead of producing empty parsed files.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16 issues found across 21 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/prompts/backend/convex-backend.ts">

<violation number="1" location="src/prompts/backend/convex-backend.ts:43">
P1: Incorrect Convex API signatures: `ctx.db.patch` and `ctx.db.replace` do not take a table name. Only `ctx.db.insert` does. These should be `ctx.db.patch(id, updates)` and `ctx.db.replace(id, data)`. The prompt's own authorization example on line ~93 correctly uses `ctx.db.patch(args.id, args.updates)` without a table name, contradicting these guidelines. This will cause the AI to generate broken Convex code.</violation>

<violation number="2" location="src/prompts/backend/convex-backend.ts:144">
P2: Inconsistent internal function path: `internal.tasks.sendReminder` references a `tasks` module, but `sendReminder` is defined in root-level `actions.ts` per the file structure and code comments. The correct path would be `internal.actions.sendReminder`. This mismatch will mislead the AI into generating incorrect scheduler references.</violation>

<violation number="3" location="src/prompts/backend/convex-backend.ts:194">
P1: The output instruction here tells the model to `Use createOrUpdateFiles to write all files`, but the parser in `backend-agent.ts` (`parseGeneratedFiles`) expects `<zapdev_file path="...">` XML blocks. This contract mismatch will cause the parser to extract zero files, triggering the `"No files were generated"` failure path.</violation>
</file>

<file name="src/agents/schema-proposal-agent.ts">

<violation number="1" location="src/agents/schema-proposal-agent.ts:50">
P2: This only checks for the opening `<schema_proposal>` tag. A truncated model response (e.g., hitting `maxOutputTokens`) could contain the opening tag but lack the closing tag or complete content, yet still be marked `success: true`. Validate both opening and closing tags to avoid propagating malformed proposals downstream.</violation>

<violation number="2" location="src/agents/schema-proposal-agent.ts:121">
P2: The `(\w+)` capture group only matches word characters, so Convex validator types like `v.id("users")` or `v.optional(v.string())` will be truncated to just `v`. This degrades the parsed field metadata stored in `parsedTables.fields`.</violation>
</file>

<file name="src/agents/backend-agent.ts">

<violation number="1" location="src/agents/backend-agent.ts:101">
P1: File paths parsed from AI output are not validated or sanitized. Since the user prompt influences the AI response, a crafted prompt could induce the model to emit paths containing `..` segments or absolute paths, enabling writes outside the intended `convex/` directory. Add path sanitization to reject traversal sequences and enforce an allowlist prefix.</violation>
</file>

<file name="src/prompts/backend/schema-proposal.ts">

<violation number="1" location="src/prompts/backend/schema-proposal.ts:53">
P2: `v.optional()` makes a field omittable (undefined), not nullable. Convex uses `v.null()` for null values. Labeling this as "nullable fields" will cause the AI to generate schemas that treat optional and nullable as interchangeable, leading to runtime mismatches when code expects `null` but gets `undefined`.</violation>

<violation number="2" location="src/prompts/backend/schema-proposal.ts:124">
P2: The relationship `users → tasks (assigned)` is labeled as "Many-to-many" but the schema uses `assignedTo: v.optional(v.id("users"))` which only allows one assigned user per task — this is a many-to-one (or one-to-many from the user side). The example will teach the AI incorrect relationship terminology.</violation>
</file>

<file name="convex/projects.ts">

<violation number="1" location="convex/projects.ts:475">
P1: This mutation is only called from Inngest but is exposed as a public `mutation`, allowing any unauthenticated client to set `hasBackend` on any project by supplying the owner's userId. Use `internalMutation` instead, which restricts access to server-side callers only.

Note: the existing `createForUser` mutation has the same problem, but this new code shouldn't perpetuate it.</violation>

<violation number="2" location="convex/projects.ts:477">
P1: Authorization check is bypassable — `userId` comes from the caller's args, not from `ctx.auth.getUserIdentity()`. Any client can call this public mutation with an arbitrary `userId` to pass the ownership check. Derive `userId` from the authenticated session instead.</violation>
</file>

<file name="src/inngest/functions.ts">

<violation number="1" location="src/inngest/functions.ts:338">
P2: These two mutations run sequentially in a single Inngest step. If `markImplementedForUser` succeeds but `setHasBackendForUser` fails, a retry will re-execute `markImplementedForUser` on an already-IMPLEMENTED row (which may or may not be idempotent). Split these into separate Inngest steps so each gets independent retry semantics and the workflow can resume from the correct point after partial failure.</violation>

<violation number="2" location="src/inngest/functions.ts:357">
P1: `useFullstackPrompt` is set to `true` based on user intent (`wantsConvexBackend`) even when the backend bootstrap failed or was skipped. This causes the code agent to receive the `FULLSTACK_PROMPT` (which instructs full Convex backend generation) without any schema proposal or pre-loaded backend files, producing Convex code that isn't tracked by the bootstrap pipeline and leaving `project.hasBackend` as `false` — triggering a duplicate bootstrap attempt on the next request.</violation>
</file>

<file name="convex/schemaProposals.ts">

<violation number="1" location="convex/schemaProposals.ts:54">
P2: Missing status guard allows a `REJECTED` proposal to be marked as `IMPLEMENTED`. The `approvedAt ?? now` fallback even backfills the approval timestamp, masking the invalid transition. Add a check that `row.status` is `"APPROVED"` (or at least not `"REJECTED"`) before proceeding.</violation>
</file>

<file name="src/prompts/backend/fullstack.ts">

<violation number="1" location="src/prompts/backend/fullstack.ts:11">
P2: The prompt hardcodes `Next.js 15.3.3` but the project uses `next@16.2.1`. Generated code will target the wrong major version, which may produce incompatible patterns or miss new APIs.</violation>

<violation number="2" location="src/prompts/backend/fullstack.ts:132">
P1: `userId: v.id("users")` expects a Convex document ID, but the query (line 151) and mutation (line 175) examples both assign `identity.subject` to it — a plain JWT subject string. This will fail Convex's runtime type validation. Either change the schema field to `v.string()`, or add a user-lookup step in the query/mutation to resolve the auth subject to a Convex `_id`.</violation>

<violation number="3" location="src/prompts/backend/fullstack.ts:197">
P2: The layout example creates a new `ConvexReactClient` inline instead of importing the one from `lib/convex.ts` (defined earlier in the prompt). This results in two separate client instances in the generated code.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.


## Output

Use createOrUpdateFiles to write all files. After all files are created, output:
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The output instruction here tells the model to Use createOrUpdateFiles to write all files, but the parser in backend-agent.ts (parseGeneratedFiles) expects <zapdev_file path="..."> XML blocks. This contract mismatch will cause the parser to extract zero files, triggering the "No files were generated" failure path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/prompts/backend/convex-backend.ts, line 194:

<comment>The output instruction here tells the model to `Use createOrUpdateFiles to write all files`, but the parser in `backend-agent.ts` (`parseGeneratedFiles`) expects `<zapdev_file path="...">` XML blocks. This contract mismatch will cause the parser to extract zero files, triggering the `"No files were generated"` failure path.</comment>

<file context>
@@ -0,0 +1,199 @@
+
+## Output
+
+Use createOrUpdateFiles to write all files. After all files are created, output:
+
+<task_summary>
</file context>
Fix with Cubic

let match;

while ((match = fileRegex.exec(text)) !== null) {
const path = match[1];
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: File paths parsed from AI output are not validated or sanitized. Since the user prompt influences the AI response, a crafted prompt could induce the model to emit paths containing .. segments or absolute paths, enabling writes outside the intended convex/ directory. Add path sanitization to reject traversal sequences and enforce an allowlist prefix.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/agents/backend-agent.ts, line 101:

<comment>File paths parsed from AI output are not validated or sanitized. Since the user prompt influences the AI response, a crafted prompt could induce the model to emit paths containing `..` segments or absolute paths, enabling writes outside the intended `convex/` directory. Add path sanitization to reject traversal sequences and enforce an allowlist prefix.</comment>

<file context>
@@ -0,0 +1,107 @@
+  let match;
+  
+  while ((match = fileRegex.exec(text)) !== null) {
+    const path = match[1];
+    const content = match[2].trim();
+    files[path] = content;
</file context>
Fix with Cubic

Comment thread convex/projects.ts
},
});

export const setHasBackendForUser = mutation({
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This mutation is only called from Inngest but is exposed as a public mutation, allowing any unauthenticated client to set hasBackend on any project by supplying the owner's userId. Use internalMutation instead, which restricts access to server-side callers only.

Note: the existing createForUser mutation has the same problem, but this new code shouldn't perpetuate it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At convex/projects.ts, line 475:

<comment>This mutation is only called from Inngest but is exposed as a public `mutation`, allowing any unauthenticated client to set `hasBackend` on any project by supplying the owner's userId. Use `internalMutation` instead, which restricts access to server-side callers only.

Note: the existing `createForUser` mutation has the same problem, but this new code shouldn't perpetuate it.</comment>

<file context>
@@ -472,6 +472,24 @@ export const createForUser = mutation({
   },
 });
 
+export const setHasBackendForUser = mutation({
+  args: {
+    userId: v.string(),
</file context>
Fix with Cubic

Comment thread src/inngest/functions.ts

const useFullstackPrompt =
Boolean(project?.hasBackend) ||
wantsConvexBackend(userPrompt) ||
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: useFullstackPrompt is set to true based on user intent (wantsConvexBackend) even when the backend bootstrap failed or was skipped. This causes the code agent to receive the FULLSTACK_PROMPT (which instructs full Convex backend generation) without any schema proposal or pre-loaded backend files, producing Convex code that isn't tracked by the bootstrap pipeline and leaving project.hasBackend as false — triggering a duplicate bootstrap attempt on the next request.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/inngest/functions.ts, line 357:

<comment>`useFullstackPrompt` is set to `true` based on user intent (`wantsConvexBackend`) even when the backend bootstrap failed or was skipped. This causes the code agent to receive the `FULLSTACK_PROMPT` (which instructs full Convex backend generation) without any schema proposal or pre-loaded backend files, producing Convex code that isn't tracked by the bootstrap pipeline and leaving `project.hasBackend` as `false` — triggering a duplicate bootstrap attempt on the next request.</comment>

<file context>
@@ -255,8 +266,99 @@ export const codeAgentFunction = inngest.createFunction(
+
+    const useFullstackPrompt =
+      Boolean(project?.hasBackend) ||
+      wantsConvexBackend(userPrompt) ||
+      Object.keys(backendPreload).length > 0;
+
</file context>
Suggested change
wantsConvexBackend(userPrompt) ||
(needsBackendBootstrap && Object.keys(backendPreload).length > 0) ||
Fix with Cubic

Comment on lines +43 to +44
- Use ctx.db.patch("table", id, updates) for partial updates
- Use ctx.db.replace("table", id, data) for full replacement
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Incorrect Convex API signatures: ctx.db.patch and ctx.db.replace do not take a table name. Only ctx.db.insert does. These should be ctx.db.patch(id, updates) and ctx.db.replace(id, data). The prompt's own authorization example on line ~93 correctly uses ctx.db.patch(args.id, args.updates) without a table name, contradicting these guidelines. This will cause the AI to generate broken Convex code.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/prompts/backend/convex-backend.ts, line 43:

<comment>Incorrect Convex API signatures: `ctx.db.patch` and `ctx.db.replace` do not take a table name. Only `ctx.db.insert` does. These should be `ctx.db.patch(id, updates)` and `ctx.db.replace(id, data)`. The prompt's own authorization example on line ~93 correctly uses `ctx.db.patch(args.id, args.updates)` without a table name, contradicting these guidelines. This will cause the AI to generate broken Convex code.</comment>

<file context>
@@ -0,0 +1,199 @@
+
+### Mutation Guidelines
+- Use ctx.db.insert("table", data) to create
+- Use ctx.db.patch("table", id, updates) for partial updates
+- Use ctx.db.replace("table", id, data) for full replacement
+- Use ctx.db.delete(id) for deletion
</file context>
Suggested change
- Use ctx.db.patch("table", id, updates) for partial updates
- Use ctx.db.replace("table", id, data) for full replacement
- Use ctx.db.patch(id, updates) for partial updates
- Use ctx.db.replace(id, data) for full replacement
Fix with Cubic

Comment thread src/inngest/functions.ts

if (backendResult.success && Object.keys(backendResult.files).length > 0) {
backendPreload = backendResult.files;
await step.run("finalize-backend-bootstrap", async () => {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: These two mutations run sequentially in a single Inngest step. If markImplementedForUser succeeds but setHasBackendForUser fails, a retry will re-execute markImplementedForUser on an already-IMPLEMENTED row (which may or may not be idempotent). Split these into separate Inngest steps so each gets independent retry semantics and the workflow can resume from the correct point after partial failure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/inngest/functions.ts, line 338:

<comment>These two mutations run sequentially in a single Inngest step. If `markImplementedForUser` succeeds but `setHasBackendForUser` fails, a retry will re-execute `markImplementedForUser` on an already-IMPLEMENTED row (which may or may not be idempotent). Split these into separate Inngest steps so each gets independent retry semantics and the workflow can resume from the correct point after partial failure.</comment>

<file context>
@@ -255,8 +266,99 @@ export const codeAgentFunction = inngest.createFunction(
+
+          if (backendResult.success && Object.keys(backendResult.files).length > 0) {
+            backendPreload = backendResult.files;
+            await step.run("finalize-backend-bootstrap", async () => {
+              const convex = getConvexClient();
+              await convex.mutation(api.schemaProposals.markImplementedForUser, {
</file context>
Fix with Cubic

Comment thread convex/schemaProposals.ts
schemaProposalId: v.id("schemaProposals"),
},
handler: async (ctx, args) => {
const row = await ctx.db.get(args.schemaProposalId);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Missing status guard allows a REJECTED proposal to be marked as IMPLEMENTED. The approvedAt ?? now fallback even backfills the approval timestamp, masking the invalid transition. Add a check that row.status is "APPROVED" (or at least not "REJECTED") before proceeding.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At convex/schemaProposals.ts, line 54:

<comment>Missing status guard allows a `REJECTED` proposal to be marked as `IMPLEMENTED`. The `approvedAt ?? now` fallback even backfills the approval timestamp, masking the invalid transition. Add a check that `row.status` is `"APPROVED"` (or at least not `"REJECTED"`) before proceeding.</comment>

<file context>
@@ -0,0 +1,66 @@
+    schemaProposalId: v.id("schemaProposals"),
+  },
+  handler: async (ctx, args) => {
+    const row = await ctx.db.get(args.schemaProposalId);
+    if (!row || row.userId !== args.userId) {
+      throw new Error("Unauthorized");
</file context>
Fix with Cubic

});

// Schedule a reminder
await ctx.scheduler.runAt(args.dueDate - 3600000, internal.tasks.sendReminder, {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Inconsistent internal function path: internal.tasks.sendReminder references a tasks module, but sendReminder is defined in root-level actions.ts per the file structure and code comments. The correct path would be internal.actions.sendReminder. This mismatch will mislead the AI into generating incorrect scheduler references.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/prompts/backend/convex-backend.ts, line 144:

<comment>Inconsistent internal function path: `internal.tasks.sendReminder` references a `tasks` module, but `sendReminder` is defined in root-level `actions.ts` per the file structure and code comments. The correct path would be `internal.actions.sendReminder`. This mismatch will mislead the AI into generating incorrect scheduler references.</comment>

<file context>
@@ -0,0 +1,199 @@
+    });
+    
+    // Schedule a reminder
+    await ctx.scheduler.runAt(args.dueDate - 3600000, internal.tasks.sendReminder, {
+      taskId,
+    });
</file context>
Fix with Cubic

import { ConvexProvider, ConvexReactClient } from "convex/react";
import { api } from "@/convex/_generated/api";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The layout example creates a new ConvexReactClient inline instead of importing the one from lib/convex.ts (defined earlier in the prompt). This results in two separate client instances in the generated code.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/prompts/backend/fullstack.ts, line 197:

<comment>The layout example creates a new `ConvexReactClient` inline instead of importing the one from `lib/convex.ts` (defined earlier in the prompt). This results in two separate client instances in the generated code.</comment>

<file context>
@@ -0,0 +1,284 @@
+import { ConvexProvider, ConvexReactClient } from "convex/react";
+import { api } from "@/convex/_generated/api";
+
+const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
</file context>
Fix with Cubic

## Tech Stack

### Frontend
- Next.js 15.3.3 with App Router
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The prompt hardcodes Next.js 15.3.3 but the project uses next@16.2.1. Generated code will target the wrong major version, which may produce incompatible patterns or miss new APIs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/prompts/backend/fullstack.ts, line 11:

<comment>The prompt hardcodes `Next.js 15.3.3` but the project uses `next@16.2.1`. Generated code will target the wrong major version, which may produce incompatible patterns or miss new APIs.</comment>

<file context>
@@ -0,0 +1,284 @@
+## Tech Stack
+
+### Frontend
+- Next.js 15.3.3 with App Router
+- React 19
+- TypeScript (strict)
</file context>
Fix with Cubic

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/inngest/functions.ts (1)

280-281: Broad regex in wantsConvexBackend may trigger expensive bootstrapping on incidental keyword mentions.

The wantsConvexBackend function (per src/agents/wants-backend.ts) uses regex matching for keywords like "database", "login", "queries". A prompt like "debug my database connection timeout" would trigger the full bootstrapping flow (schema proposal agent + backend implementer + Convex mutations).

Consider either:

  • Tightening the regex to require more explicit backend intent
  • Adding a lightweight LLM classifier for ambiguous cases
  • Documenting this behavior as intentional for broad coverage
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/inngest/functions.ts` around lines 280 - 281, The current broad regex in
wantsConvexBackend causes incidental keywords to trigger needsBackendBootstrap
(the Boolean using wantsConvexBackend in the declaration of
needsBackendBootstrap), so update the wantsConvexBackend function to reduce
false positives: either tighten the regex to require stronger intent phrases
(e.g., match whole-word phrases like "build backend", "create database schema",
"implement login/auth", or require verbs + backend nouns) or implement a
lightweight LLM/heuristic secondary check for ambiguous matches and return true
only when both checks indicate backend intent; ensure the needsBackendBootstrap
line continues to call wantsConvexBackend(userPrompt) but relies on the revised
stricter logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/inngest/functions.ts`:
- Around line 407-408: The object literal sets the `system` property twice
(first as `useFullstackPrompt ? FULLSTACK_PROMPT : PROMPT`, then immediately as
`PROMPT`), which overwrites the conditional value; remove the duplicate
unconditional `system: PROMPT` so the code uses the conditional `system` value
based on `useFullstackPrompt` (look for the object containing `system`,
`useFullstackPrompt`, `FULLSTACK_PROMPT`, and `PROMPT` in this file and keep
only the conditional `system` entry).
- Around line 303-334: The flow currently persists a schema proposal with status
"PENDING" and immediately calls runBackendImplementerAgent even when
parseSchemaProposal may return no tables; update the logic in the block handling
schemaProposalResult.success so that after calling parseSchemaProposal you
validate parsed.tables.length > 0 (and/or parsed.relationships as appropriate)
before invoking runBackendImplementerAgent, and if validation fails do not call
runBackendImplementerAgent—either set a different proposal status or require
explicit approval; reference parseSchemaProposal, schemaProposalResult, the
persisted proposal step ("persist-schema-proposal") and
runBackendImplementerAgent when making the change.

---

Nitpick comments:
In `@src/inngest/functions.ts`:
- Around line 280-281: The current broad regex in wantsConvexBackend causes
incidental keywords to trigger needsBackendBootstrap (the Boolean using
wantsConvexBackend in the declaration of needsBackendBootstrap), so update the
wantsConvexBackend function to reduce false positives: either tighten the regex
to require stronger intent phrases (e.g., match whole-word phrases like "build
backend", "create database schema", "implement login/auth", or require verbs +
backend nouns) or implement a lightweight LLM/heuristic secondary check for
ambiguous matches and return true only when both checks indicate backend intent;
ensure the needsBackendBootstrap line continues to call
wantsConvexBackend(userPrompt) but relies on the revised stricter logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 79ea7413-e7ad-43d7-a463-9c798b9a96d9

📥 Commits

Reviewing files that changed from the base of the PR and between 739784e and 0c010fc.

📒 Files selected for processing (5)
  • convex/messages.ts
  • convex/projects.ts
  • src/inngest/functions.ts
  • src/modules/home/ui/components/project-form.tsx
  • src/modules/projects/ui/components/message-form.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/modules/projects/ui/components/message-form.tsx

Comment thread src/inngest/functions.ts
Comment on lines +303 to +334
if (schemaProposalResult.success) {
schemaProposalText = schemaProposalResult.schemaProposal;
const parsed = parseSchemaProposal(schemaProposalResult.schemaProposal);
const schemaProposalRowId = await step.run("persist-schema-proposal", async () => {
const convex = getConvexClient();
return await convex.mutation(api.schemaProposals.createForUser, {
userId,
projectId,
messageId: triggerMessageId,
proposal: schemaProposalResult.schemaProposal,
parsedTables:
parsed.tables.length > 0
? parsed.tables.map((t) => ({
name: t.name,
purpose: t.purpose,
fields: t.fields,
indexes: t.indexes,
}))
: undefined,
parsedRelationships:
parsed.relationships.length > 0 ? parsed.relationships : undefined,
status: "PENDING",
});
});

const backendResult = await step.run("backend-implementer", () =>
runBackendImplementerAgent(
userPrompt,
schemaProposalResult.schemaProposal,
plan || undefined
)
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Schema proposal proceeds to backend generation without validation or approval.

The flow runs runBackendImplementerAgent immediately after creating the schema proposal with status: "PENDING" (line 324), without any user approval step. Additionally, parseSchemaProposal silently returns empty arrays if the LLM output is malformed (per src/agents/schema-proposal-agent.ts:100), meaning invalid proposals still trigger backend generation.

This contradicts the <convex_context> message at line 382 which states "Schema design (approved)". Consider either:

  1. Adding validation that parsed.tables.length > 0 before proceeding
  2. Removing the "(approved)" label if auto-approval is intentional
🛡️ Optional: Add validation before backend generation
         if (schemaProposalResult.success) {
           schemaProposalText = schemaProposalResult.schemaProposal;
           const parsed = parseSchemaProposal(schemaProposalResult.schemaProposal);
+          
+          if (parsed.tables.length === 0) {
+            console.warn("[BACKEND] Schema proposal parsing failed - skipping backend generation");
+          } else {
           const schemaProposalRowId = await step.run("persist-schema-proposal", async () => {
             // ... existing code
           });
+          }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/inngest/functions.ts` around lines 303 - 334, The flow currently persists
a schema proposal with status "PENDING" and immediately calls
runBackendImplementerAgent even when parseSchemaProposal may return no tables;
update the logic in the block handling schemaProposalResult.success so that
after calling parseSchemaProposal you validate parsed.tables.length > 0 (and/or
parsed.relationships as appropriate) before invoking runBackendImplementerAgent,
and if validation fails do not call runBackendImplementerAgent—either set a
different proposal status or require explicit approval; reference
parseSchemaProposal, schemaProposalResult, the persisted proposal step
("persist-schema-proposal") and runBackendImplementerAgent when making the
change.

Comment thread src/inngest/functions.ts
Comment on lines +407 to 408
system: useFullstackPrompt ? FULLSTACK_PROMPT : PROMPT,
system: PROMPT,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Duplicate system property causes build failure and incorrect behavior.

Line 407 conditionally sets system based on useFullstackPrompt, but line 408 unconditionally overwrites it with PROMPT. This causes:

  1. TypeScript compilation error (pipeline is failing)
  2. The fullstack prompt will never be used even when useFullstackPrompt is true
🐛 Proposed fix — remove the duplicate property
     const codeAgent = createAgent<AgentState>({
       name: "code-agent",
       description: "An expert coding agent",
       system: useFullstackPrompt ? FULLSTACK_PROMPT : PROMPT,
-      system: PROMPT,
       tool_choice: "auto",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
system: useFullstackPrompt ? FULLSTACK_PROMPT : PROMPT,
system: PROMPT,
const codeAgent = createAgent<AgentState>({
name: "code-agent",
description: "An expert coding agent",
system: useFullstackPrompt ? FULLSTACK_PROMPT : PROMPT,
tool_choice: "auto",
🧰 Tools
🪛 Biome (2.4.10)

[error] 407-407: This property is later overwritten by an object member with the same name.

(lint/suspicious/noDuplicateObjectKeys)

🪛 GitHub Actions: CI

[error] 408-408: TypeScript type check failed: An object literal cannot have multiple properties with the same name. Duplicate 'system' property at line 408.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/inngest/functions.ts` around lines 407 - 408, The object literal sets the
`system` property twice (first as `useFullstackPrompt ? FULLSTACK_PROMPT :
PROMPT`, then immediately as `PROMPT`), which overwrites the conditional value;
remove the duplicate unconditional `system: PROMPT` so the code uses the
conditional `system` value based on `useFullstackPrompt` (look for the object
containing `system`, `useFullstackPrompt`, `FULLSTACK_PROMPT`, and `PROMPT` in
this file and keep only the conditional `system` entry).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant